package se.sll.itintegration.catalogue.gui.service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.security.RolesAllowed;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.session.InvalidSessionException;
import se.sll.itintegration.catalogue.gui.data.EmailAddress;
import se.sll.itintegration.catalogue.gui.ejb.UserEjb;
import se.sll.itintegration.catalogue.gui.entity.CareUnitEntity;
import se.sll.itintegration.catalogue.gui.entity.UserEntity;
import se.sll.service.catalogue.common.Role;
import se.sll.service.catalogue.common.exception.*;
import se.sll.service.catalogue.common.validation.FacadeRESTValidator;
import se.sll.service.catalogue.gui.api.model.CareUnit;
import se.sll.service.catalogue.gui.api.model.UserAddToUnit;
import se.sll.service.catalogue.gui.api.model.UserCreate;
import se.sll.service.catalogue.gui.api.model.UserList;
import se.sll.service.catalogue.gui.api.resource.UsersService;

/**
 *
 * @author khaleddaham
 * @author Jiri Uosukainen
 */
//@Path("/users")
@Slf4j
@Stateless
public class UserFacadeREST implements UsersService {

    @EJB
    private UserEjb userEjb;
    private static final String T_MEDIA_TYPE = "application/json; charset=UTF-8";

    @Inject
    private Validator validator;

    @Inject
    private HsaCache hsaCache;

    @Context SecurityContext sc;

    private final FacadeRESTValidator facadeRESTValidator = new FacadeRESTValidator();

    /**
     * For unit testing.
     */
    void setUserEjb(UserEjb userEjb) {
        this.userEjb = userEjb;
    }

    /**
     * For unit testing.
     */
    void setValidator(Validator validator) {
        this.validator = validator;
    }

    /**
     * For unit testing.
     */
    void setHsaCache(HsaCache hsaCache) {
        this.hsaCache = hsaCache;
    }

    /**
     * Get user
     *
     * @param hsaId
     * @return
     * @throws se.sll.service.catalogue.common.exception.ServiceException
     */
    /*@GET
    @Path("/{hsaId}")
    @Produces(T_MEDIA_TYPE)*/
    @Override
    public GetUsersByHsaIdResponse getUsersByHsaId(@PathParam("hsaId") String hsaId)
            throws ServiceException {

        log.debug("UserFacadeREST.read() ENTER");
        log.debug("UserFacadeREST.read(): hsaId = {}", hsaId);

        checkAdminOrSelf(hsaId, sc, ServiceErrorCode.REQUIRES_ADMIN);

        UserList user = null;
        List<UserEntity> entities = null;
        try {
            entities = userEjb.findByAttributeValue("hsaID", hsaId);
        } catch (RuntimeException e) {
            log.error("UserFacadeREST.read() failed", e);
            throw new ServiceCatalogueException(); // TODO Add message
        }
        // Some checking 
        if (entities.size() == 1) {
            user = transform(entities.get(0), true);
        } else if (entities.isEmpty()) {
            throw new ResourceNotFoundException(ServiceErrorCode.USER_NOT_FOUND, hsaId);
        } else {
            // hsaId is not unique --> Error
            log.error("UserFacadeREST.read() [hsaId:{}] is not unique", hsaId);
            throw new ServiceCatalogueException(ServiceErrorCode.USER_NAME_NOT_UNIQUE);
        }

        log.debug("UserFacadeREST.read() EXIT");
        return GetUsersByHsaIdResponse.jsonOK(user);
    }

    /**
     * Update user
     *
     * @param hsaId
     * @param entity
     * @return
     * @throws ServiceException
     */
    /*@PUT
    @Path("/{hsaId}")
    @Consumes(T_MEDIA_TYPE)
    @Produces(T_MEDIA_TYPE)*/
    @Override
    public PutUsersByHsaIdResponse putUsersByHsaId(@PathParam("hsaId") String hsaId, UserCreate entity)
            throws ServiceException {
        log.debug("UserFacadeREST.update() ENTER");
        log.debug("Update user: " + hsaId + ", data: " + entity);
        touchUserSession();

        checkAdminOrSelf(hsaId, sc, ServiceErrorCode.REQUIRES_ADMIN);
        facadeRESTValidator.validate(entity, validator);

        String newHsaId = entity.getHsaId();
        if (newHsaId != null && !hsaId.equals(newHsaId)) {
            throw new ValidationException(ServiceErrorCode.USER_NAME_CANNOT_BE_CHANGED);
        }

        UserList returnUser;
        List<UserEntity> existing = userEjb.findByAttributeValue("hsaID", hsaId);
        if (existing.size() == 1) {
            UserEntity mergeUser = existing.get(0);

            String email = entity.getEmail();
            mergeUser.setEmail(email);

            String password = entity.getPassword();
            if (password != null) {
                String passwordHash = SecurityUtils.createPasswordHash(password);
                mergeUser.setPassword(passwordHash);
            }

            String realname = entity.getRealname();
            mergeUser.setRealname(realname);

            String hsaIdCheck = entity.getHsaId();
            if (hsaIdCheck != null && !hsaIdCheck.equals(mergeUser.getHsaID())) {
                checkAdmin(sc, ServiceErrorCode.REQUIRES_ADMIN_HSAID);
                mergeUser.setHsaID(hsaIdCheck);
            }

            UserEntity.STATUS status = entity.getStatus() == null ? null : UserEntity.STATUS.valueOf(entity.getStatus());
            if (status != null && !status.equals(mergeUser.getStatus())) {
                checkAdmin(sc, ServiceErrorCode.REQUIRES_ADMIN_STATUS);
                mergeUser.setStatus(status);
            }

            String role = entity.getRole();
            if (role != null && UserEntity.ROLES.valueOf(role) != mergeUser.getRole()) {
                checkAdmin(sc, ServiceErrorCode.REQUIRES_ADMIN_ROLE);
                mergeUser.setRole(UserEntity.ROLES.valueOf(role));
            }

            List<CareUnit> units = entity.getCareUnitID();
            if (units != null && units.size() > 0) {
                // Map existing units by hsaId
                Set<CareUnitEntity> mergeUnits = mergeUser.getCareUnits();
                Map<String, CareUnitEntity> mergeUnitMap = new HashMap<>();
                for (CareUnitEntity mergeUnit : mergeUnits) {
                    mergeUnitMap.put(mergeUnit.getHsaID(), mergeUnit);
                }

                // Translate new units, using existing map when possible
                Set<CareUnitEntity> newUnits = new HashSet<>();
                for (CareUnit unit : units) {
                    CareUnitEntity newUnit = mergeUnitMap.get(unit.getHsaId());
                    if (newUnit == null) {
                        newUnit = userEjb.updateCareUnit(unit.getHsaId());
                    }
                    newUnits.add(newUnit);
                }

                if (!newUnits.equals(mergeUnits)) {
                    checkAdmin(sc, ServiceErrorCode.REQUIRES_ADMIN_UNIT_ID);
                    mergeUser.setCareUnits(newUnits);
                }
            }

            try {
                userEjb.edit(mergeUser);
            } catch (RuntimeException e) {
                log.error("UserFacadeREST.updateUser() failed", e);
                throw new ServiceCatalogueException(); // TODO: Add message
            }
            returnUser = transform(mergeUser, true);
        } else {
            throw new ResourceNotFoundException(ServiceErrorCode.USER_NOT_FOUND, hsaId);
        }
        log.debug("UserFacadeREST.update() EXIT");
        return PutUsersByHsaIdResponse.jsonOK(returnUser);
    }

    private void touchUserSession() {
        try {
            org.apache.shiro.SecurityUtils.getSubject().getSession().touch();
        } catch (InvalidSessionException |UnavailableSecurityManagerException e) {
        }
    }

    /**
     * Create user
     *
     * @param entity
     * @return
     * @throws ServiceException
     */
    /*@POST
    @RolesAllowed(Role.SUPERADMIN)
    @Consumes(T_MEDIA_TYPE)
    @Produces(T_MEDIA_TYPE)*/
    @Override
    public PostUsersResponse postUsers(UserCreate entity) throws ServiceException {
        log.debug("Creating user: " + entity);
        touchUserSession();

        facadeRESTValidator.validate(entity, validator);
        String password = entity.getPassword();
        if (password == null || password.isEmpty()) {
            throw new ValidationException(ServiceErrorCode.INVALID_PASSWORD_EMPTY);
        }

        UserList returnUser;
        List<UserEntity> existing = userEjb.findByAttributeValue("hsaID", entity.getHsaId());
        if (existing.isEmpty()) {
            try {
                UserEntity ue = new UserEntity();
                ue.setCreatedByHsaID("");
                ue.setEmail(entity.getEmail());
                ue.setHsaID(entity.getHsaId());
                String passwordHash = SecurityUtils.createPasswordHash(password);
                ue.setPassword(passwordHash);
                ue.setRealname(entity.getRealname());
                ue.setSourceSystemHsaID("");
                ue.setStatus(UserEntity.STATUS.valueOf(entity.getStatus()));
                ue.setRole(UserEntity.ROLES.valueOf(entity.getRole()));

                Set<CareUnitEntity> units = new HashSet<>();
                for (CareUnit unit : entity.getCareUnitID()) {
                    String unitID = unit.getHsaId();
                    CareUnitEntity cue = userEjb.updateCareUnit(unitID);
                    units.add(cue);
                }
                ue.setCareUnits(units);

                log.debug("Create UserEntity.");
                userEjb.create(ue);
                returnUser = transform(ue, true);
            } catch (RuntimeException e) {
                log.error("UserFacadeREST.create() failed", e);
                throw new ServiceCatalogueException(); // TODO: Add message
            }
        } else {
            throw new ServiceCatalogueException(ServiceErrorCode.USER_ALREADY_EXISTS, entity.getHsaId());
        }
        return PostUsersResponse.jsonCreated(returnUser);
    }

    /**
     * Remove user
     *
     * @param hsaId
     * @throws se.sll.service.catalogue.common.exception.ServiceException
     */
    /*@DELETE
    @Path("/{hsaId}")
    @RolesAllowed(Role.SUPERADMIN)
    @Produces(T_MEDIA_TYPE)*/
    @Override
    public DeleteUsersByHsaIdResponse deleteUsersByHsaId(@PathParam("hsaId") String hsaId) throws ServiceException {
        touchUserSession();
        List<UserEntity> existing = userEjb.findByAttributeValue("hsaID", hsaId);
        if (existing == null || existing.isEmpty()) {
            throw new ResourceNotFoundException(ServiceErrorCode.USER_NOT_FOUND);
        }
        for (UserEntity ue : existing) {
            userEjb.remove(ue);
        }

        return DeleteUsersByHsaIdResponse.withoutContent();
    }

    /**
     * Update user role
     *
     * @param hsaId
     * @param role
     * @throws se.sll.service.catalogue.common.exception.ServiceException
     */
    /*@PUT
    @Path("/{hsaId}/role/{role}")*/
    @RolesAllowed(Role.SUPERADMIN)
    //@Produces(T_MEDIA_TYPE)
    @Override
    public PutUsersByHsaIdRoleByRoleResponse putUsersByHsaIdRoleByRole(@PathParam("role") String role,
                                                                       @PathParam("hsaId") String hsaId)
            throws ServiceException {
        log.debug("Update role for user: " + hsaId + ", role: " + role);
        touchUserSession();
        List<UserEntity> existing = userEjb.findByAttributeValue("hsaID", hsaId);
        if (existing == null || existing.isEmpty()) {
            throw new ResourceNotFoundException(ServiceErrorCode.USER_NOT_FOUND, hsaId);
        }
        UserEntity.ROLES newRole;
        try {
            newRole = UserEntity.ROLES.valueOf(role);
        } catch (IllegalArgumentException e) {
            throw new ValidationException(ServiceErrorCode.INVALID_ROLE);
        }

        UserEntity mergeUser = existing.get(0);
        mergeUser.setRole(newRole);
        try {
            userEjb.edit(mergeUser);
        } catch (Exception ex) {
            throw new ValidationException(ServiceErrorCode.UNKNOWN);
        }

        return PutUsersByHsaIdRoleByRoleResponse.withoutContent();
    }

    /**
     * Get all users NOTE: careUnit is not returned
     *
     * @return
     * @throws se.sll.service.catalogue.common.exception.ServiceException
     */
    //@GET
    @RolesAllowed(Role.SUPERADMIN)
    //@Produces(T_MEDIA_TYPE)
    @Override
    public GetUsersResponse getUsers() throws ServiceException {
        log.debug("UserFacadeREST.find() ENTER");

        List<UserList> users = new ArrayList<>();
        try {
            List<UserEntity> list = userEjb.findAll();
            for (UserEntity userEntity : list) {
                users.add(transform(userEntity, false));
            }
        } catch (RuntimeException e) {
            log.error("UserFacadeREST.find() failed", e);
            throw new ServiceCatalogueException(); //TODO Add message
        }
        log.debug("UserFacadeREST.find() EXIT");
        return GetUsersResponse.jsonOK(users);
    }

    /**
     * Get all users for the given unit
     *
     * @param unitId
     * @return The list of users on the given unit.
     * @throws se.sll.service.catalogue.common.exception.ServiceException
     */
    /*@GET
    @Path("/units/{unitId}")*/
    @RolesAllowed(Role.SUPERADMIN)
    //@Produces(T_MEDIA_TYPE)
    @Override
    public GetUsersUnitsByUnitIdResponse getUsersUnitsByUnitId(@PathParam("unitId") String unitId) throws ServiceException {
        //    public List<User> findUsersForUnit(@PathParam("unitId") String unitId) {
        log.debug("UserFacadeREST.findUsersForUnit() ENTER");

        List<UserList> users = new ArrayList<>();
        try {
            Set<UserEntity> set = userEjb.findForUnit(unitId);
            for (UserEntity userEntity : set) {
                users.add(transform(userEntity, false));
            }
        } catch (RuntimeException e) {
            log.error("UserFacadeREST.findUsersForUnit() failed", e);
            throw new ServiceCatalogueException(); //TODO Add message
        }
        log.debug("UserFacadeREST.findUsersForUnit() EXIT");
        return GetUsersUnitsByUnitIdResponse.jsonOK(users);
    }

    /**
     * Sets users for the given unit
     *
     * @param entity
     * @throws ServiceException
     */
    /*@PUT
    @Path("/units/setusers")*/
    @RolesAllowed(Role.SUPERADMIN)
    //@Produces(T_MEDIA_TYPE)
    @Override
    public PutUsersUnitsSetusersResponse putUsersUnitsSetusers(UserAddToUnit entity) throws ServiceException {
        log.debug("UserFacadeREST.setUsersForUnit() ENTER");
        touchUserSession();

        facadeRESTValidator.validate(entity, validator);
        log.debug("Validation passed: {}", entity);

        try {
            userEjb.setUsersToUnit(entity.getUnitId(), entity.getHsaId());
        } catch (Exception e) {
            log.error("UserFacadeREST.setUsersForUnit() failed", e);
            throw new ServiceCatalogueException(); //TODO Add message
        }
        log.debug("UserFacadeREST.setUsersForUnit() EXIT");
        return PutUsersUnitsSetusersResponse.withoutContent();
    }

    /**
     * Finds email addresses for administrators for the given units. This method
     * will return emails for users with roles <code>CAREUNITADMIN</code> and
     * <code>CAPACITYADMIN</code>.
     *
     * @param entity A list of unit id's (HSAID)
     * @return Object containing a List of unique email addresses
     * @throws ServiceException
     */
    /*@POST
    @Path("/emails")*/
    @RolesAllowed(Role.SUPERADMIN)
    /*@Consumes(T_MEDIA_TYPE)
    @Produces(T_MEDIA_TYPE)*/
    public PostUsersEmailsResponse postUsersEmails(List<String> entity) throws ServiceException {
        log.debug("UserFacadeREST.findAdminEmails(): ENTER");
        touchUserSession();

        List<String> emails = new ArrayList<>();

        log.debug("Units: " + entity);
        if (entity == null || entity.isEmpty()) {
            throw new ValidationException(ServiceErrorCode.INVALID_UNIT_IDS_EMPTY);
        }

        try {
            List<UserEntity.ROLES> roles = Arrays.asList(UserEntity.ROLES.CAPACITYADMIN, UserEntity.ROLES.CAREUNITADMIN);
            Set<EmailAddress> found = this.findEmailsByUnitAndRoles(entity, roles);
            if (found != null) {
                //emails.addAll(found);
                ArrayList<String> mailList = new ArrayList<>();
                for (EmailAddress emailAddress : found) {
                    mailList.add(emailAddress.getEmailAddress());
                }
                emails.addAll(mailList);
            }
        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.findAdminEmails() failed", e);
            throw new ServiceCatalogueException();
        }

        log.debug("UserFacadeREST.findAdminEmails(): EXIT ");
        return PostUsersEmailsResponse.jsonOK(emails);
    }

    protected void testSetSecurityContext(SecurityContext sc) {
        this.sc = sc;
    }

    /**
     * Finds email addresses for users with specified roles on the given units.
     *
     * @param unitIds A list of unit id's (HSAID)
     * @param roles The roles of the users
     * @return Set of email addresses
     */
    private Set<EmailAddress> findEmailsByUnitAndRoles(List<String> unitIds, List<UserEntity.ROLES> roles)
            throws ServiceException {
        log.debug("UserFacadeREST.findEmailsByUnitAndRoles(): ENTER");
        log.debug("Units: " + unitIds);
        log.debug("Roles:" + roles);

        touchUserSession();
        if (unitIds == null || unitIds.isEmpty()) {
            throw new ValidationException(ServiceErrorCode.INVALID_UNIT_IDS_EMPTY);
        }
        if (roles == null) {
            throw new ValidationException(ServiceErrorCode.INVALID_ROLES_EMPTY);
        }

        Set<EmailAddress> emails = new HashSet<>();
        try {
            Set<UserEntity> users = userEjb.findUsersByUnitAndRoles(unitIds, roles);

            for (UserEntity user : users) {

//                Validator validator = validatorFactory.getValidator();
                EmailAddress mail = new EmailAddress(user.getEmail());
                Set<ConstraintViolation<EmailAddress>> constraintViolations = validator.validate(mail);

                if (constraintViolations.isEmpty()) {
                    emails.add(mail);
                } else {
                    for (ConstraintViolation<EmailAddress> cv : constraintViolations) {
                        log.warn("Attribute: '{}', {}", new Object[]{cv.getPropertyPath(), cv.getMessage()});
                    }
                    log.warn("Found user registered with invalid email address [user.hsaId:{}, user.realname:{}, user.email:{}]", new Object[]{user.getHsaID(), user.getRealname(), user.getEmail()});
                }
            }
        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.findEmailsByUnitAndRole() failed", e);
            throw new ServiceCatalogueException();
        }

        log.debug("UserFacadeREST.findEmailsByUnitAndRoles(): EXIT ");
        return emails;
    }

    /**
     * Checks that principal is admin.
     *
     * @param sc
     * @throws NotAuthorizedException
     */
    private void checkAdmin(SecurityContext sc, ServiceErrorCode errorCode, Object... args) throws NotAuthorizedException {
        if (sc.isUserInRole(Role.SUPERADMIN)) {
            return;
        }
        throw new NotAuthorizedException(errorCode, args);
    }

    /**
     * Checks that principal is admin or equal to subject.
     *
     * @param subject
     * @param sc
     * @throws NotAuthorizedException
     */
    private void checkAdminOrSelf(String subject, SecurityContext sc, ServiceErrorCode errorCode, Object... args) throws NotAuthorizedException {
        if (sc.isUserInRole(Role.SUPERADMIN)) {
            return;
        }
        if (sc.getUserPrincipal() != null && sc.getUserPrincipal().getName().equals(subject)) {
            return;
        }
        throw new NotAuthorizedException(errorCode, args);
    }

    private UserList transform(UserEntity userEntity, boolean includeUnits) {
        UserList user = null;

        if (userEntity != null) {
            user = new UserList();
            user.setCreatedByHsaId(userEntity.getCreatedByHsaID());
            user.setCreationTime(userEntity.getCreationTime());
            user.setEmail(userEntity.getEmail());
            user.setHsaId(userEntity.getHsaID());
            user.setLastLogin(userEntity.getLastLogin());
            user.setRealname(userEntity.getRealname());
            user.setRole(userEntity.getRole().name());
            user.setSourceSystemHsaId(userEntity.getSourceSystemHsaID());
            user.setStatus(userEntity.getStatus().name());
            user.setUpdatedTime(userEntity.getUpdatedTime());

            List<CareUnit> units = new ArrayList<>();
            user.setCareUnits(units);
            if (includeUnits) {
                for (CareUnitEntity careUnitEntity : userEntity.getCareUnits()) {
                    CareUnit unit = new CareUnit();
                    unit.setHsaId(careUnitEntity.getHsaID());
                    if (hsaCache.getHsa(unit.getHsaId()) != null) {

                        unit.setDisplayName(hsaCache.getHsa(unit.getHsaId()).getRelativeDistinguishedName());
                    }
                    units.add(unit);
                }
            }
        }
        return user;
    }
}
